package org.spring.boot.example.mybatis.plugin;

import java.sql.Connection;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.sql.Statement;
import java.util.LinkedList;
import java.util.List;

import org.apache.ibatis.executor.statement.BaseStatementHandler;
import org.apache.ibatis.executor.statement.RoutingStatementHandler;
import org.apache.ibatis.executor.statement.StatementHandler;
import org.apache.ibatis.logging.Log;
import org.apache.ibatis.logging.LogFactory;
import org.apache.ibatis.mapping.MappedStatement;
import org.apache.ibatis.plugin.Interceptor;
import org.apache.ibatis.plugin.Intercepts;
import org.apache.ibatis.plugin.Invocation;
import org.apache.ibatis.plugin.Signature;
import org.apache.ibatis.reflection.SystemMetaObject;
import org.apache.ibatis.session.ResultHandler;

@Intercepts({
		@Signature(args = { Statement.class, ResultHandler.class }, method = "query", type = StatementHandler.class),
		@Signature(args = { Statement.class }, method = "queryCursor", type = StatementHandler.class) })
public class MysqlSlowSqlExplainInterceptor implements Interceptor {

	protected static final Log log = LogFactory.getLog(MysqlSlowSqlExplainInterceptor.class);
	
	protected long slowTime;

	public MysqlSlowSqlExplainInterceptor(long slowTime) {
		this.slowTime = slowTime;
	}
	
	@Override
	public Object intercept(Invocation invocation) throws Throwable {
		long startTime = System.currentTimeMillis();
		Object result = invocation.proceed();
		long diffTime = System.currentTimeMillis() - startTime;

		if (slowTime > diffTime) {
			return result;
		}

		StatementHandler handler = (StatementHandler) invocation.getTarget();
		if (handler instanceof RoutingStatementHandler) {
			handler = (StatementHandler) SystemMetaObject.forObject(handler).getValue("delegate");
		}
		Statement statement = (Statement) invocation.getArgs()[0];
		Connection connection = statement.getConnection();
		if (!connection.getMetaData().getURL().contains(":mysql:")) {
			return result;
		}

		String sql = getSQL(statement);
		String explainSql = "explain " + sql;
		handler.parameterize(statement);
		PreparedStatement ps = connection.prepareStatement(explainSql);
		ps.execute();
		ResultSet resultSet = ps.getResultSet();
		List<ExplainInfo> infos = handlerResultSet(resultSet);
		SlowSqlExecuteInfo executeInfo = new SlowSqlExecuteInfo();
		executeInfo.setSql(sql);
		executeInfo.setExecuteTime(diffTime);
		executeInfo.setExplainInfos(infos);

		if (handler instanceof BaseStatementHandler) {
			MappedStatement mappedStatement = (MappedStatement) SystemMetaObject.forObject(handler)
					.getValue("mappedStatement");
			executeInfo.setStatementId(mappedStatement.getId());
		}
		handlerExecuteInfo(executeInfo);
		return result;
	}

	protected void handlerExecuteInfo(SlowSqlExecuteInfo executeInfo) {
		String message = "time: %s, id: %s , sql: %s, explainInfo:[%s]";
		log.warn(String.format(message, executeInfo.getExecuteTime(), executeInfo.getStatementId(), executeInfo.getSql(), executeInfo.getExplainInfos()));
	}

	@Override
	public Object plugin(Object target) {
		if (target instanceof StatementHandler) {
			return Interceptor.super.plugin(target);
		}
		return target;
	}

	private static String getSQL(Statement pre) {
		String toString = pre.toString();
		return toString.substring(toString.lastIndexOf(":") + 1).trim();
	}

	private List<ExplainInfo> handlerResultSet(ResultSet resultSet) throws SQLException {
		List<ExplainInfo> infos = new LinkedList<>();
		while (resultSet.next()) {
			ExplainInfo info = new ExplainInfo();
			info.setId(resultSet.getInt("id"));
			info.setSelectType(resultSet.getString("select_type"));
			info.setTable(resultSet.getString("table"));
			info.setPartitions(resultSet.getString("partitions"));
			info.setType(resultSet.getString("type"));
			info.setPossibleKeys(resultSet.getString("possible_keys"));
			info.setKey(resultSet.getString("key"));
			info.setKeyLen(resultSet.getLong("key_len"));
			info.setRef(resultSet.getString("ref"));
			info.setRows(resultSet.getLong("rows"));
			info.setFiltered(resultSet.getLong("filtered"));
			info.setExtra(resultSet.getString("Extra"));
			infos.add(info);
		}

		return infos;
	}

	public static interface SlowCallback {
		void call(SlowSqlExecuteInfo executeInfo);
	}

}
